Skip to content

Add settings to Model base class #2136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

svilupp
Copy link
Contributor

@svilupp svilupp commented Jul 5, 2025

Fixes #2119

The implementation follows the suggestion by @DouweM

This PR:

  • implements a settings field in the Model class
  • adapts the settings resolution to go in this order: model settings < agent model settings < run-specific model settings
  • adds 2 updates to the docs to mention how to use this feature with FallbackModel and the new settings hierarchy
  • tests for the settings handling - it's in a separate file as it's a separate behavior, or should I merge it with other tests?

# Issues Encountered

  • Found various "special" model classes inheriting from the core Model superclass (instrumentedmodel, testmodel, wrappermodel, fallbackmodel)

I dediced on the following behaviour -- any thoughts?

  • Wrapper/Fallback Models: Should use the underlying model's settings, not have their own
  • Instrumented Models: Keep existing behaviour/fields unchanged (it literary has a field called settings, so I had to change the field definition to be self.settings: ModelSettings | InstrumentationSettings | None = settings
  • TestModel: Added settings field for API consistency

Next Steps

  • Agree the correct behavior/design for the complicated model classes
  • Perhaps I could refactor some wrapper code (tried to resolve the right settings in agent.run()) to be a separate utility with its own tests?

Copy link
Contributor

hyperlint-ai bot commented Jul 5, 2025

PR Change Summary

Enhanced the Model base class by introducing a settings field and updating the documentation to reflect the new settings hierarchy.

  • Implemented a settings field in the Model class.
  • Established a settings resolution order: model settings, agent model settings, and run-specific model settings.
  • Updated documentation to explain the new settings feature and hierarchy.
  • Added tests for settings handling.

Modified Files

  • docs/agents.md
  • docs/models/index.md

How can I customize these reviews?

Check out the Hyperlint AI Reviewer docs for more information on how to customize the review.

If you just want to ignore it on this PR, you can add the hyperlint-ignore label to the PR. Future changes won't trigger a Hyperlint review.

Note specifically for link checks, we only check the first 30 links in a file and we cache the results for several hours (for instance, if you just added a page, you might experience this). Our recommendation is to add hyperlint-ignore to the PR to ignore the link check for this PR.

@DouweM DouweM self-assigned this Jul 7, 2025
Copy link
Contributor

@DouweM DouweM left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@svilupp Thanks for working on this!

# Merge model settings from any Model
if isinstance(model_used, InstrumentedModel):
# For InstrumentedModel, get settings from the wrapped model
wrapped_model_settings = getattr(model_used.wrapped, 'settings', None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we define settings on WrapperModel (which InstrumentedModel inherits from) to return self.wrapped.settings, so we don't need to do this special-casing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That may require us to rename the field on Model to _settings, so that we can define settings as a property that can be overridden on WrapperModel.

model_settings = merge_model_settings(wrapped_model_settings, model_settings)
else:
# For regular models, use their settings directly
current_settings = getattr(model_used, 'settings', None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be model_used.settings?


def __init__(
self,
wrapped: Model | KnownModelName,
options: InstrumentationSettings | None = None,
) -> None:
super().__init__(wrapped)
self.settings = options or InstrumentationSettings()
# Store instrumentation settings separately from model settings
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this comment or the next one, with the different field names it's clear enough what's happening

# Store instrumentation settings separately from model settings
self.instrumentation_settings = options or InstrumentationSettings()
# Initialize base Model with no settings to avoid storing InstrumentationSettings there
Model.__init__(self, settings=None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we make _settings a field on Model with default value None (like _profile currently is), we can skip this init.

@@ -42,6 +42,10 @@ class MCPSamplingModel(Model):
[`ModelSettings`][pydantic_ai.settings.ModelSettings], so this value is used as fallback.
"""

def __post_init__(self):
"""Initialize the base Model class."""
super().__init__()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can likely also remove this if _settings: ModelSettings | None = None becomes a Model field.

# Run with additional settings
run_settings = ModelSettings(temperature=0.8, seed=42)

# This should work without errors and properly merge the settings
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we verify it ends up with the correct settings?

def test_gemini_model_settings():
"""Test that GeminiModel can be initialized with settings."""
if not gemini_available or GeminiModel is None: # pragma: no cover
return # Skip if dependencies not available
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use pytest.skip here, there are some examples in the codebase

return ModelResponse(parts=[TextPart('test')])

# Replace the instrumented model's wrapped model with a function model for testing
instrumented_model.wrapped = FunctionModel(test_response, settings=base_model_settings)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's create a new InstrumentedModel instead of reusing the existing one, as WrapperModel caches some properties so changing wrapped in place is not handled correctly across the board.

2. **Agent-level defaults** - Set during [`Agent`][pydantic_ai.agent.Agent] initialization via the `model_settings` argument. These override model defaults.
3. **Run-time overrides** - Passed to `run{_sync,_stream}` functions via the `model_settings` argument. These have the highest priority and override both agent and model defaults.

**Settings Precedence**: Run-time > Agent > Model
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can drop this line as the list already explicitly states the priority.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, it may be useful to write here that the settings are merged, not replaced.

@@ -124,6 +124,39 @@ The `ModelResponse` message above indicates in the `model_name` field that the o
!!! note
Each model's options should be configured individually. For example, `base_url`, `api_key`, and custom clients should be set on each model itself, not on the `FallbackModel`.

### Per-Model Settings

You can configure different `ModelSettings` for each model in a fallback chain by passing the `settings` parameter when creating each model. This is particularly useful when different providers have different optimal configurations:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
You can configure different `ModelSettings` for each model in a fallback chain by passing the `settings` parameter when creating each model. This is particularly useful when different providers have different optimal configurations:
You can configure different [`ModelSettings`][pydantic_ai.settings.ModelSettings] for each model in a fallback chain by passing the `settings` parameter when creating each model. This is particularly useful when different providers have different optimal configurations:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

FallbackModel to allow model_settings specific to each model
3 participants